﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using AmiEmuLib.AmigaMachine.AmigaCPU;
using AmiEmuLib.AmigaConfiguration;
using AmiEmuLib.AmigaMachine;
using System.Xml;
using System.IO;
using AmiEmuLib.AmigaMachine.AmigaMemory;
using System.Xml.Serialization;
using TomanuExtensions;

namespace AmiEmuLib.SaveState
{
    /// <summary>
    /// Save state. Full or diff.
    /// </summary>
    public class State
    {
        /// <summary>
        /// Using to create unique ID.
        /// </summary>
        internal static int Counter = 0;

        /// <summary>
        /// Previous state. 
        /// </summary>
        public State Prev { get; private set; }

        /// <summary>
        /// Unique ID. Used for binding in StateManager.
        /// </summary>
        public int ID { get; private set; }

        /// <summary>
        /// CPU state. Whe diff can be null when same as Prev.
        /// </summary>
        public CPU680x0State CPUState { get; internal set; }

        /// <summary>
        /// Memory state. Whe diff can be null when same as Prev. 
        /// When diff contains only banks with some data.
        /// </summary>
        public MemoryState MemoryState { get; private set; }

        /// <summary>
        /// Is this is key node.
        /// </summary>
        public bool Key { get; private set; }

        /// <summary>
        /// Config for this state.
        /// </summary>
        public AmigaConfig Config { get; private set; }

        /// <summary>
        /// Constructor.
        /// </summary>
        private State()
        {
        }

        /// <summary>
        /// Create state from machine.
        /// </summary>
        /// <param name="a_machine"></param>
        public State(Machine a_machine)
            : this(a_machine, null, false)
        {
        }

        /// <summary>
        /// Constructor. Create diff state witout states manager.
        /// </summary>
        /// <param name="a_machine"></param>
        /// <param name="a_prev"></param>
        public State(Machine a_machine, State a_prev)
            : this(a_machine, a_prev, false)
        {
        }

        /// <summary>
        /// Constructor. Used by states manager.
        /// </summary>
        /// <param name="a_machine"></param>
        /// <param name="a_prev"></param>
        /// <param name="a_key"></param>
        internal State(Machine a_machine, State a_prev = null, bool a_key = false)
        {
            ID = Counter++;

            Prev = a_prev;
            Key = a_key;

            if (a_prev != null)
            {
                MemoryState = new MemoryState(a_machine.Memory, a_prev.MemoryState);
                CPUState = CPU680x0State.Create(a_machine.CPU, a_prev.CPUState);
            }
            else
            {
                Config = new AmigaConfig(a_machine.Config);
                MemoryState = new MemoryState(a_machine.Memory);
                CPUState = CPU680x0State.Create(a_machine.CPU);
            }
        }

        /// <summary>
        /// Is root state.
        /// </summary>
        internal bool Root
        {
            get
            {
                return Key && (Prev == null);
            }
        }

        /// <summary>
        /// Read xml.
        /// </summary>
        /// <param name="a_reader"></param>
        private void ReadXml(XmlReader a_reader, Func<int, State> a_state_resolver)
        {
            ID = a_reader.GetAttributeInt("ID");

            if (a_reader.MoveToAttribute("PrevID"))
            {
                int PrevID = a_reader.GetAttributeInt("PrevID");
                Prev = a_state_resolver(PrevID);
            }

            Key = a_reader.GetAttributeBoolDef("Key");

            if (Prev != null)
                MemoryState = new MemoryState(Prev.MemoryState);
            else
                MemoryState = new MemoryState();

            if (Prev != null)
                CPUState = CPU680x0State.Create(Prev.CPUState);

            a_reader.MoveToElement();

            if (!a_reader.IsStartElement(GetType().Name))
                throw new XmlException();

            if (a_reader.IsEmptyElement)
                return;

            a_reader.ReadStartElement(GetType().Name);

            if (a_reader.IsStartElement(typeof(AmigaConfig).Name))
                Config = new AmigaConfig(a_reader);

            if (a_reader.Name == typeof(CPU68000State).Name)
            {
                if (CPUState == null)
                    CPUState = new CPU68000State();
                CPUState.ReadXml(a_reader);
            }
            else if (a_reader.Name == typeof(CPU68020State).Name)
            {
                if (CPUState == null)
                    CPUState = new CPU68020State();
                CPUState.ReadXml(a_reader);
            }
            else if (CPUState == null)
                throw new XmlException();

            if (a_reader.Name == MemoryState.GetType().Name)
                MemoryState.ReadXml(a_reader);

            a_reader.ReadEndElement();
        }

        /// <summary>
        /// Write xml.
        /// </summary>
        /// <param name="a_writer"></param>
        private void WriteXml(XmlWriter a_writer)
        {
            a_writer.WriteStartElement(GetType().Name);

            a_writer.WriteAttribute("ID", ID);

            if (Prev != null)
                a_writer.WriteAttribute("PrevID", Prev.ID);

            if (Key)
                a_writer.WriteAttribute("Key", Key);

            if (Config != null)
                Config.WriteXml(a_writer);

            if ((CPUState.Prev == null) || !CPUState.Equals(CPUState.Prev))
            {
                a_writer.WriteStartElement(CPUState.GetType().Name);
                CPUState.WriteXml(a_writer);
                a_writer.WriteEndElement();
            }

            if (MemoryState.Banks.Count() != 0)
            {
                a_writer.WriteStartElement(MemoryState.GetType().Name);
                MemoryState.WriteXml(a_writer);
                a_writer.WriteEndElement();
            }

            a_writer.WriteEndElement();
        }

        /// <summary>
        /// Load full state from file. 
        /// </summary>
        /// <param name="a_fileName"></param>
        /// <returns></returns>
        public static State LoadFromXml(string a_fileName)
        {
            using (FileStream fs = new FileStream(a_fileName, FileMode.Open, FileAccess.Read))
                return LoadFromXml(fs);
        }

        /// <summary>
        /// Load full state from file.
        /// </summary>
        /// <param name="a_stream"></param>
        /// <returns></returns>
        public static State LoadFromXml(Stream a_stream)
        {
            return LoadFromXml(a_stream, (id) => null);
        }

        /// <summary>
        /// Load diff, key or root state. Used by state manager.
        /// </summary>
        /// <param name="a_stream"></param>
        /// <param name="a_states_mgr"></param>
        /// <returns></returns>
        internal static State LoadFromXml(Stream a_stream, StatesManager a_states_mgr)
        {
            return LoadFromXml(a_stream, (id) => a_states_mgr.Lookup(id));
        }

        /// <summary>
        /// Load diff, key or root state. Used from outside state manager.
        /// </summary>
        /// <param name="a_stream"></param>
        /// <param name="a_states_mgr"></param>
        /// <returns></returns>
        public static State LoadFromXml(Stream a_stream, State a_prev)
        {
            return LoadFromXml(a_stream, (id) => (id == a_prev.ID) ? a_prev : null);
        }

        /// <summary>
        /// Load diff, key or root state.
        /// </summary>
        /// <param name="a_stream"></param>
        /// <param name="a_prev"></param>
        /// <returns></returns>
        private static State LoadFromXml(Stream a_stream, Func<int, State> a_state_resolver)
        {
            State state = new State();
            XmlReaderExtensions.ReadXml(a_stream, (reader) => 
                state.ReadXml(reader, a_state_resolver));
            return state;
        }

        /// <summary>
        /// Save state to file.
        /// </summary>
        /// <param name="a_fileName"></param>
        public void Save(string a_fileName)
        {
            using (FileStream fs = new FileStream(a_fileName, FileMode.Create))
                Save(fs);
        }

        /// <summary>
        /// Save state to stream.
        /// </summary>
        /// <param name="a_stream"></param>
        public void Save(Stream a_stream)
        {
            XmlWriterExtensions.WriteXml(a_stream, (writer) => WriteXml(writer));
        }

        /// <summary>
        /// Find first not null config.
        /// </summary>
        /// <returns></returns>
        public AmigaConfig GetConfig()
        {
            if (Config != null)
                return Config;
            else
                return Prev.GetConfig();
        }

        /// <summary>
        /// Restore state into machine. Machine should have same config as that in state.
        /// </summary>
        /// <param name="a_machine"></param>
        public void Restore(Machine a_machine)
        {
            CPUState.Restore(a_machine.CPU);
            MemoryState.Restore(a_machine.Memory);
        }

        public override bool Equals(object a_obj)
        {
            if (a_obj == null)
                return false;
            State state = a_obj as State;
            if (state == null)
                return false;

            if ((state.Prev != null) ^ (Prev != null))
                throw new NotImplementedException();

            if (state.Prev != null)
                if (!state.Prev.Equals(Prev))
                    return false;
             
            if (Config != null)
                if (!Config.Equals(state.Config))
                    return false;

            if (!CPUState.Equals(state.CPUState))
                return false;

            if (!MemoryState.Equals(state.MemoryState))
                return false;

            return true;
        }

        public override int GetHashCode()
        {
            return ((Prev != null) ? Prev.ID.GetHashCode() : 0) ^
                   ((Config != null) ? Config.GetHashCode() : 0) ^ 
                   CPUState.GetHashCode() ^ MemoryState.GetHashCode();
        }
    }
}
